(function () {
  const nextTickCbs = [];
  const nextTick = function (cb) {
    nextTickCbs.push(cb);
    if (nextTickCbs.length === 1) {
      requestIdleCallback(() => {
        nextTickCbs.forEach(cb => cb());
        nextTickCbs.length = 0;
      });
    }
  };
  const queue = [];
  let has = {};
  let waiting = false;
  let flushing = false;
  let index = 0;
  const queueWatcher = function (watcher) {
    const id = watcher.id;
    if (has[id]) return;
    has[id] = true;
    if (!flushing) {
      queue.push(watcher);
    } else {
      const i = queue.length - 1;
      while (i > index && queue[i].id > watcher.id) {
        i--;
      }
      queue.splice(i + 1, 0, watcher);
    }
    if (waiting) return;
    waiting = true;
    nextTick(flushSchedulerQueue);
  };

  const flushSchedulerQueue = function () {
    flushing = true;
    let watcher, id;

    queue.sort((a, b) => a.id - b.id);

    for (index = 0; index < queue.length; index++) {
      watcher = queue[index];
      id = watcher.id;
      has[id] = null;
      watcher.run(); // TODO: handler circular updates
    }

    index = queue.length = 0;
    has = {};
    waiting = flushing = false;
  };

  window.queueWatcher = queueWatcher;
  window.nextTick = nextTick;
})();